Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new templated MIP row-scaling strategy (mip_scaling_strategy_t), wires it into MIP solver, heuristics, and tests, enables mip scaling by default, adds timer-based early exits in solver flows, and updates build and includes to compile the new scaling implementation. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
cpp/src/mip_heuristics/utils.cuh (1)
342-344: Consider replacing the magic1e-6with a named constant or a tolerance parameter.The hardcoded
1e-6here is inconsistent with the rest of the codebase, which derives near-zero thresholds from solver-configured tolerances (e.g.,tols.absolute_tolerancepassed throughget_cstr_tolerance). Callers have no way to adjust this threshold for problems with very small or very large objective scales — exactly the situation this PR is addressing with scaling.♻️ Suggested refactor
+static constexpr double compute_rel_mip_gap_zero_tol = 1e-6; + template <typename f_t> -f_t compute_rel_mip_gap(f_t user_obj, f_t solution_bound) +f_t compute_rel_mip_gap(f_t user_obj, f_t solution_bound, f_t zero_tol = static_cast<f_t>(compute_rel_mip_gap_zero_tol)) { - if (integer_equal<f_t>(user_obj, 0.0, 1e-6)) { - return integer_equal<f_t>(solution_bound, 0.0, 1e-6) ? 0.0 - : std::numeric_limits<f_t>::infinity(); + if (integer_equal<f_t>(user_obj, f_t{0}, zero_tol)) { + return integer_equal<f_t>(solution_bound, f_t{0}, zero_tol) ? f_t{0} + : std::numeric_limits<f_t>::infinity(); } return std::abs(user_obj - solution_bound) / std::abs(user_obj); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/mip_heuristics/utils.cuh` around lines 342 - 344, Replace the hardcoded 1e-6 near-zero threshold in the integer_equal checks with a named tolerance or configurable parameter: stop using the literal in the integer_equal<f_t>(user_obj, 0.0, 1e-6) and integer_equal<f_t>(solution_bound, 0.0, 1e-6) calls and instead pass a named constant (e.g., near_zero_tolerance) or the solver-configured tolerance obtained from get_cstr_tolerance/tols.absolute_tolerance supplied into the function; update the function signature or call sites around integer_equal usage so callers can control the tolerance for user_obj and solution_bound comparisons.cpp/src/pdlp/solve.cu (1)
1001-1007: Avoid unconditional dual-simplex conversion in inside-MIP concurrent mode.
dual_simplex_problemis constructed even whensettings.inside_mipis true and the dual-simplex thread is skipped. That conversion can be deferred to the non-MIP branch to reduce setup overhead.♻️ Suggested refactor sketch
- dual_simplex::user_problem_t<i_t, f_t> dual_simplex_problem = - cuopt_problem_to_simplex_problem<i_t, f_t>(problem.handle_ptr, problem); + std::unique_ptr<dual_simplex::user_problem_t<i_t, f_t>> dual_simplex_problem_ptr; + if (!settings.inside_mip) { + dual_simplex_problem_ptr = std::make_unique<dual_simplex::user_problem_t<i_t, f_t>>( + cuopt_problem_to_simplex_problem<i_t, f_t>(problem.handle_ptr, problem)); + } dual_simplex::user_problem_t<i_t, f_t> barrier_problem = settings.inside_mip ? cuopt_problem_to_simplex_problem<i_t, f_t>(problem.handle_ptr, *concurrent_problem) - : dual_simplex_problem; + : *dual_simplex_problem_ptr;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/pdlp/solve.cu` around lines 1001 - 1007, Currently dual_simplex_problem is always constructed via cuopt_problem_to_simplex_problem even when settings.inside_mip is true and the dual-simplex thread is skipped; change the logic so you only call cuopt_problem_to_simplex_problem to create dual_simplex_problem inside the non-MIP branch and, when settings.inside_mip is true, set barrier_problem from cuopt_problem_to_simplex_problem(..., *concurrent_problem) without constructing dual_simplex_problem. Update the block that defines dual_simplex_problem and barrier_problem (referencing dual_simplex_problem, barrier_problem, cuopt_problem_to_simplex_problem, settings.inside_mip, concurrent_problem) to defer or eliminate the unconditional conversion and only perform the conversion needed for the chosen branch.cpp/src/mip_heuristics/solve.cu (1)
142-149: Feasibility OR (is_feasible_after_unscaling || is_feasible_before_scaling) biases toward accepting solutions.This means a solution that is feasible only in the scaled space but not in the original problem space will still be reported as feasible to the user. While the warning at Line 143 alerts the user about a mismatch, the downstream consumer of
mip_solution_twill see a feasible status regardless of which space the solution is actually feasible in.This is a reasonable pragmatic choice for MIP heuristics (scaling-induced numerical drift shouldn't reject an otherwise good solution), but consider whether reporting a
NumericalIssuesflag alongside the feasible solution would give users more diagnostic visibility.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/mip_heuristics/solve.cu` around lines 142 - 149, The current call to scaled_sol.get_solution uses (is_feasible_after_unscaling || is_feasible_before_scaling) which can mark a solution feasible even if it only holds in the scaled space; change this so the reported feasibility reflects the original problem (use is_feasible_after_unscaling for the primary feasible flag) and add a diagnostic flag on the returned mip_solution_t (e.g., NumericalIssues or scaling_mismatch) when is_feasible_before_scaling != is_feasible_after_unscaling; update the call site in scaled_sol.get_solution and the mip_solution_t construction/definition to accept and propagate this diagnostic so consumers see both the canonical feasibility and any scaling-related numerical issue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/src/mip_heuristics/diversity/diversity_manager.cu`:
- Around line 204-220: The presolve time check is only gating the presolve
block, but context.scaling.scale_problem() (symbol: scale_problem) can still run
after presolve time expires; update the logic to re-check the presolve timer
(presolve_timer.check_time_limit()) before invoking scaling or include the
scaling call inside the same timed branch so that scaling only runs when
presolve time remains and when context.settings.mip_scaling is true; adjust
around the existing conditional that uses presolve_timer.check_time_limit(),
trivial_presolve, and context.settings.mip_scaling to ensure scale_problem() is
not called if the timer indicates time is exhausted.
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu`:
- Around line 281-290: row_skip_scaling is read (e.g., by thrust::count into i_t
skipped_big_m_rows) even when compute_big_m_skip_rows(...) is disabled, leaving
it uninitialized and making skip logic nondeterministic; initialize
row_skip_scaling to a safe default (e.g., all zeros) before any read when
compute_big_m_skip_rows is not called. Locate uses of row_skip_scaling and
compute_big_m_skip_rows in mip_scaling_strategy.cu (the first count into
skipped_big_m_rows and the later block around lines 305-333) and insert a
deterministic initialization (for example via thrust::fill with the same thrust
policy) so row_skip_scaling has defined values whenever compute_big_m_skip_rows
is skipped.
In `@cpp/src/pdlp/solve.cu`:
- Around line 1080-1086: The unscale step only mutates solution vectors via
concurrent_scaling_ptr->unscale_solutions but leaves metadata (primal_objective,
dual_objective, l2_primal_residual, l2_dual_residual) computed earlier from the
scaled problem (via convert_dual_simplex_sol), causing inconsistency; after
calling concurrent_scaling_ptr->unscale_solutions for sol_pdlp and sol_barrier,
recompute or update those metadata fields from the now-unscaled vectors (e.g.,
invoke the same logic used in convert_dual_simplex_sol or explicitly recompute
objective and L2 residuals from sol_pdlp.get_primal_solution(),
get_dual_solution(), get_reduced_cost() and similarly for sol_barrier) so
returned solutions have metadata matching the unscaled original-space values.
---
Nitpick comments:
In `@cpp/src/mip_heuristics/solve.cu`:
- Around line 142-149: The current call to scaled_sol.get_solution uses
(is_feasible_after_unscaling || is_feasible_before_scaling) which can mark a
solution feasible even if it only holds in the scaled space; change this so the
reported feasibility reflects the original problem (use
is_feasible_after_unscaling for the primary feasible flag) and add a diagnostic
flag on the returned mip_solution_t (e.g., NumericalIssues or scaling_mismatch)
when is_feasible_before_scaling != is_feasible_after_unscaling; update the call
site in scaled_sol.get_solution and the mip_solution_t construction/definition
to accept and propagate this diagnostic so consumers see both the canonical
feasibility and any scaling-related numerical issue.
In `@cpp/src/mip_heuristics/utils.cuh`:
- Around line 342-344: Replace the hardcoded 1e-6 near-zero threshold in the
integer_equal checks with a named tolerance or configurable parameter: stop
using the literal in the integer_equal<f_t>(user_obj, 0.0, 1e-6) and
integer_equal<f_t>(solution_bound, 0.0, 1e-6) calls and instead pass a named
constant (e.g., near_zero_tolerance) or the solver-configured tolerance obtained
from get_cstr_tolerance/tols.absolute_tolerance supplied into the function;
update the function signature or call sites around integer_equal usage so
callers can control the tolerance for user_obj and solution_bound comparisons.
In `@cpp/src/pdlp/solve.cu`:
- Around line 1001-1007: Currently dual_simplex_problem is always constructed
via cuopt_problem_to_simplex_problem even when settings.inside_mip is true and
the dual-simplex thread is skipped; change the logic so you only call
cuopt_problem_to_simplex_problem to create dual_simplex_problem inside the
non-MIP branch and, when settings.inside_mip is true, set barrier_problem from
cuopt_problem_to_simplex_problem(..., *concurrent_problem) without constructing
dual_simplex_problem. Update the block that defines dual_simplex_problem and
barrier_problem (referencing dual_simplex_problem, barrier_problem,
cuopt_problem_to_simplex_problem, settings.inside_mip, concurrent_problem) to
defer or eliminate the unconditional conversion and only perform the conversion
needed for the chosen branch.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
benchmarks/linear_programming/cuopt/run_mip.cppcpp/include/cuopt/linear_programming/mip/solver_settings.hppcpp/src/mip_heuristics/CMakeLists.txtcpp/src/mip_heuristics/diversity/diversity_manager.cucpp/src/mip_heuristics/diversity/population.cucpp/src/mip_heuristics/diversity/recombiners/sub_mip.cuhcpp/src/mip_heuristics/mip_scaling_strategy.cucpp/src/mip_heuristics/mip_scaling_strategy.cuhcpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cucpp/src/mip_heuristics/solve.cucpp/src/mip_heuristics/solver.cucpp/src/mip_heuristics/solver.cuhcpp/src/mip_heuristics/solver_context.cuhcpp/src/mip_heuristics/utils.cuhcpp/src/pdlp/initial_scaling_strategy/initial_scaling.cucpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuhcpp/src/pdlp/solve.cucpp/src/pdlp/swap_and_resize_helper.cuhcpp/src/pdlp/utils.cuhcpp/src/routing/utilities/check_input.cucpp/src/utilities/copy_helpers.hppcpp/src/utilities/cuda_helpers.cuhcpp/tests/mip/feasibility_jump_tests.cucpp/tests/mip/load_balancing_test.cucpp/tests/mip/multi_probe_test.cupython/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py
💤 Files with no reviewable changes (4)
- python/cuopt_server/cuopt_server/utils/linear_programming/data_definition.py
- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuh
- cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cu
- cpp/src/mip_heuristics/relaxed_lp/relaxed_lp.cu
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
cpp/src/mip_heuristics/mip_scaling_strategy.cu (1)
260-263:row_skip_scalingis now correctly zero-initialized before use.
thrust::fillat Lines 261-262 ensuresrow_skip_scalingis deterministic beforecompute_big_m_skip_rowspopulates it. This resolves the prior uninitialized-read risk.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu` around lines 260 - 263, The prior uninitialized-read risk for row_skip_scaling is fixed by explicitly zero-initializing it; ensure you keep the thrust::fill call that uses handle_ptr_->get_thrust_policy() to fill row_skip_scaling.begin()..end() with i_t(0) before calling compute_big_m_skip_rows so compute_big_m_skip_rows only reads initialized values from row_skip_scaling; confirm the allocation of iteration_scaling remains unchanged and placed after row_skip_scaling is initialized.
🧹 Nitpick comments (2)
cpp/src/mip_heuristics/mip_scaling_strategy.cu (2)
66-80: Preferthrust::maximum<T>/thrust::minimum<T>over custommax_op_t/min_op_t.Both structs are direct re-implementations of standard Thrust functors that are already
__host__ __device__-compatible and accepted by CUB'sDeviceSegmentedReduce::Reduce.♻️ Proposed refactor
-template <typename t_t> -struct max_op_t { - __host__ __device__ t_t operator()(const t_t& lhs, const t_t& rhs) const - { - return lhs > rhs ? lhs : rhs; - } -}; - -template <typename t_t> -struct min_op_t { - __host__ __device__ t_t operator()(const t_t& lhs, const t_t& rhs) const - { - return lhs < rhs ? lhs : rhs; - } -};Then replace usages with
thrust::maximum<f_t>{}andthrust::minimum<f_t>{}at call sites.As per coding guidelines: "Avoid reinventing functionality already available in Thrust, CCCL, or RMM libraries."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu` around lines 66 - 80, Replace the custom functors max_op_t and min_op_t with the standard Thrust functors: remove or stop using max_op_t and min_op_t and update all call sites to use thrust::maximum<f_t>{} and thrust::minimum<f_t>{} (or the appropriate template type instead of f_t) so the code leverages the existing __host__ __device__-compatible Thrust implementations accepted by CUB::DeviceSegmentedReduce::Reduce.
104-177:compute_big_m_skip_rowsrecomputesrow_inf_normredundantly, and ignores the passedtemp_storagestream.Two issues:
- The function hard-codes
op_problem.handle_ptr->get_stream()for its CUB calls (unlikecompute_row_inf_normwhich accepts astream_viewparameter). This inconsistency would silently break if a caller ever passes a different stream.- The max-abs segmented reduction on Lines 121-130 recomputes
row_inf_normthat is already computed in the first call tocompute_row_inf_normon loop iteration 0 (Line 301). Sincecompute_big_m_skip_rowsis called once before the loop,row_inf_normgets computed twice before any scaling is applied.Consider adding a
stream_viewparameter tocompute_big_m_skip_rowsconsistent withcompute_row_inf_norm, and passing the already-computedrow_inf_normfrom a precedingcompute_row_inf_normcall instead of re-running the max reduce.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu` around lines 104 - 177, compute_big_m_skip_rows currently reruns the max-abs segmented reduction into row_inf_norm and hard-codes op_problem.handle_ptr->get_stream(), causing redundant work and a stream mismatch; change the signature of compute_big_m_skip_rows to accept a raft::stream_view (or stream_view alias used elsewhere) and remove the initial max-abs cub::DeviceSegmentedReduce call so the function uses the row_inf_norm that was produced by compute_row_inf_norm; also replace all uses of op_problem.handle_ptr->get_stream() in compute_big_m_skip_rows with the new stream_view and ensure temp_storage/temp_storage_bytes and other CUB/Thrust calls use that stream_view when invoked; finally update callers to compute_big_m_skip_rows to pass the stream_view and to call compute_row_inf_norm once beforehand so row_inf_norm is not recomputed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu`:
- Around line 327-370: You apply per-iteration scaling (iteration_scaling) to
coefficients and bounds but never record the accumulated per-row scaling
factors, so later dual unscaling can't be performed; add and update a per-row
accumulator in op_problem_scaled_ (e.g., op_problem_scaled_.row_scaling_factors
initialized to 1) and multiply it by the iteration scaling for each row each
iteration. Concretely: locate iteration_scaling, coefficient_row_index, and the
three thrust::transform calls that multiply op_problem_scaled_.coefficients,
.constraint_lower_bounds, and .constraint_upper_bounds; add a thrust::transform
to update op_problem_scaled_.row_scaling_factors by applying iteration_scaling
(use a permutation iterator with coefficient_row_index when mapping scalings
through coefficient indices to row indices for coefficients, and a direct
iterator for constraint bounds) so the per-row product is maintained for
post-solve dual unscaling.
---
Duplicate comments:
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu`:
- Around line 260-263: The prior uninitialized-read risk for row_skip_scaling is
fixed by explicitly zero-initializing it; ensure you keep the thrust::fill call
that uses handle_ptr_->get_thrust_policy() to fill
row_skip_scaling.begin()..end() with i_t(0) before calling
compute_big_m_skip_rows so compute_big_m_skip_rows only reads initialized values
from row_skip_scaling; confirm the allocation of iteration_scaling remains
unchanged and placed after row_skip_scaling is initialized.
---
Nitpick comments:
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu`:
- Around line 66-80: Replace the custom functors max_op_t and min_op_t with the
standard Thrust functors: remove or stop using max_op_t and min_op_t and update
all call sites to use thrust::maximum<f_t>{} and thrust::minimum<f_t>{} (or the
appropriate template type instead of f_t) so the code leverages the existing
__host__ __device__-compatible Thrust implementations accepted by
CUB::DeviceSegmentedReduce::Reduce.
- Around line 104-177: compute_big_m_skip_rows currently reruns the max-abs
segmented reduction into row_inf_norm and hard-codes
op_problem.handle_ptr->get_stream(), causing redundant work and a stream
mismatch; change the signature of compute_big_m_skip_rows to accept a
raft::stream_view (or stream_view alias used elsewhere) and remove the initial
max-abs cub::DeviceSegmentedReduce call so the function uses the row_inf_norm
that was produced by compute_row_inf_norm; also replace all uses of
op_problem.handle_ptr->get_stream() in compute_big_m_skip_rows with the new
stream_view and ensure temp_storage/temp_storage_bytes and other CUB/Thrust
calls use that stream_view when invoked; finally update callers to
compute_big_m_skip_rows to pass the stream_view and to call compute_row_inf_norm
once beforehand so row_inf_norm is not recomputed.
| thrust::transform( | ||
| handle_ptr_->get_thrust_policy(), | ||
| thrust::make_zip_iterator(thrust::make_tuple(row_inf_norm.begin(), row_skip_scaling.begin())), | ||
| thrust::make_zip_iterator(thrust::make_tuple(row_inf_norm.end(), row_skip_scaling.end())), | ||
| iteration_scaling.begin(), | ||
| [target_norm] __device__(auto row_info) -> f_t { | ||
| const f_t row_norm = thrust::get<0>(row_info); | ||
| const i_t skip_row = thrust::get<1>(row_info); | ||
| if (skip_row || row_norm == f_t(0)) { return f_t(1); } | ||
|
|
||
| const f_t desired_scaling = target_norm / row_norm; | ||
| if (!isfinite(desired_scaling) || desired_scaling <= f_t(0)) { return f_t(1); } | ||
|
|
||
| int exponent = 0; | ||
| const f_t mantissa = | ||
| frexp(desired_scaling, &exponent); // desired_scaling = mantissa * 2^exponent | ||
| int k = | ||
| mantissa >= static_cast<f_t>(nearest_pow2_mantissa_threshold) ? exponent : exponent - 1; | ||
| // clamp it so we don't overscale. range [1e-6,1e6] | ||
| if (k < row_scaling_k_min) { k = row_scaling_k_min; } | ||
| if (k > row_scaling_k_max) { k = row_scaling_k_max; } | ||
| return ldexp(f_t(1), k); | ||
| }); | ||
|
|
||
| thrust::transform( | ||
| handle_ptr_->get_thrust_policy(), | ||
| op_problem_scaled_.coefficients.begin(), | ||
| op_problem_scaled_.coefficients.end(), | ||
| thrust::make_permutation_iterator(iteration_scaling.begin(), coefficient_row_index.begin()), | ||
| op_problem_scaled_.coefficients.begin(), | ||
| thrust::multiplies<f_t>{}); | ||
|
|
||
| thrust::transform(handle_ptr_->get_thrust_policy(), | ||
| op_problem_scaled_.constraint_lower_bounds.begin(), | ||
| op_problem_scaled_.constraint_lower_bounds.end(), | ||
| iteration_scaling.begin(), | ||
| op_problem_scaled_.constraint_lower_bounds.begin(), | ||
| thrust::multiplies<f_t>{}); | ||
| thrust::transform(handle_ptr_->get_thrust_policy(), | ||
| op_problem_scaled_.constraint_upper_bounds.begin(), | ||
| op_problem_scaled_.constraint_upper_bounds.end(), | ||
| iteration_scaling.begin(), | ||
| op_problem_scaled_.constraint_upper_bounds.begin(), | ||
| thrust::multiplies<f_t>{}); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for row_scaling field definitions and usages across the codebase
rg -n "row_scal" --type cu --type cpp --type hpp --type cuh -B2 -A2Repository: NVIDIA/cuopt
Length of output: 82
🏁 Script executed:
# Search for unscaling/unscale logic in solver files
rg -n "unscal\|unscale" --type cu --type cpp --type hpp -B2 -A2 | head -100Repository: NVIDIA/cuopt
Length of output: 82
🏁 Script executed:
# Check the problem_t structure definition to see if it has scaling fields
rg -n "struct.*problem_t|class.*problem_t" --type cu --type cpp --type hpp -A20Repository: NVIDIA/cuopt
Length of output: 82
🏁 Script executed:
# Search for pdlp_initial_scaling_strategy to understand how it handles scaling
find . -type f \( -name "*pdlp*scaling*" -o -name "*initial_scaling*" \)Repository: NVIDIA/cuopt
Length of output: 174
🏁 Script executed:
# Look for references to mip_scaling_strategy and how scaling is managed
rg -n "mip_scaling_strategy|scale_problem" --type cu --type cpp --type hpp -B1 -A3 | head -80Repository: NVIDIA/cuopt
Length of output: 82
🏁 Script executed:
# Find files related to scaling and problem structure
fd -e cu -e cpp -e hpp -e cuh | grep -E "(scaling|problem)" | head -30Repository: NVIDIA/cuopt
Length of output: 1410
🏁 Script executed:
# Search for row_scaling in all text files
rg "row_scal" -B2 -A2 | head -100Repository: NVIDIA/cuopt
Length of output: 1988
🏁 Script executed:
# Search for unscal in all files
rg "unscal" -B2 -A2Repository: NVIDIA/cuopt
Length of output: 30069
🏁 Script executed:
# Look at the main file under review
wc -l cpp/src/mip_heuristics/mip_scaling_strategy.cuRepository: NVIDIA/cuopt
Length of output: 106
🏁 Script executed:
# Check the problem_t structure - search for "struct problem_t" or "class problem_t"
rg "struct.*problem_t|class.*problem_t" -A25 | head -150Repository: NVIDIA/cuopt
Length of output: 11838
🏁 Script executed:
# Look for scaling accumulation or storage in mip_scaling_strategy.cu
rg "scaling_factor" -B1 -A1Repository: NVIDIA/cuopt
Length of output: 33378
🏁 Script executed:
# Look at mip_scaling_strategy.cu to understand context and usage
head -100 cpp/src/mip_heuristics/mip_scaling_strategy.cuRepository: NVIDIA/cuopt
Length of output: 3426
🏁 Script executed:
# Find where scale_problem is called and how scaled solutions are handled
rg "scale_problem|op_problem_scaled_" cpp/src/mip_heuristics/ -A3 -B1Repository: NVIDIA/cuopt
Length of output: 7867
🏁 Script executed:
# Check if MIP solver extracts dual variables from LP relaxations
rg "get_dual|multiplier|shadow" cpp/src/mip_heuristics/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 4421
🏁 Script executed:
# Look at the problem_t class definition used in mip_scaling_strategy
sed -n '1,100p' cpp/src/mip_heuristics/problem/problem.cuhRepository: NVIDIA/cuopt
Length of output: 4144
🏁 Script executed:
# Check if PDLP's initial_scaling_strategy is used and stores row scaling
rg "initial_scaling_strategy|row_scaling_factors" cpp/src/mip_heuristics/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 38
🏁 Script executed:
# Check where LP dual solutions are used after being retrieved
rg "lp_dual_optimal_solution|get_dual_solution" cpp/src/mip_heuristics/ -B3 -A5 | head -100Repository: NVIDIA/cuopt
Length of output: 8936
🏁 Script executed:
# Check if PDLP is called internally by MIP and if it needs stored row scaling
rg "pdlp|PDLP" cpp/src/mip_heuristics/diversity/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 8305
🏁 Script executed:
# Look at the solution class to see what fields it has
sed -n '1,150p' cpp/src/mip_heuristics/solution/solution.cuhRepository: NVIDIA/cuopt
Length of output: 6391
🏁 Script executed:
# Check if there is any unscaling logic after scale_problem in MIP workflow
rg -A20 "scale_problem\(\)" cpp/src/mip_heuristics/diversity/diversity_manager.cu | head -50Repository: NVIDIA/cuopt
Length of output: 1095
🏁 Script executed:
# Verify what happens with the scaled problem - is it used for LP solving or something else
rg "op_problem_scaled_|is_scaled_" cpp/src/mip_heuristics/diversity/diversity_manager.cu -B2 -A2Repository: NVIDIA/cuopt
Length of output: 38
🏁 Script executed:
# Check what PDLP's unscale_solutions actually does
sed -n '1,80p' cpp/src/pdlp/initial_scaling_strategy/initial_scaling.cuRepository: NVIDIA/cuopt
Length of output: 3200
🏁 Script executed:
# See where scale_problem is called and what happens to the scaled problem after
rg -A30 "context.scaling.scale_problem()" cpp/src/mip_heuristics/diversity/diversity_manager.cu | head -60Repository: NVIDIA/cuopt
Length of output: 1646
🏁 Script executed:
# Check if the scaled problem is used directly by PDLP or if it's unscaled first
rg "solve_lp_with_method|get_relaxed_lp_solution" cpp/src/mip_heuristics/ -B5 -A5 | head -60Repository: NVIDIA/cuopt
Length of output: 5367
🏁 Script executed:
# Check if there's any unscaling logic applied to duals after PDLP returns them
rg "unscale.*dual|dual.*unscale" cpp/src/mip_heuristics/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 38
🏁 Script executed:
# Check what callback receives and if duals need to be in original space
rg "set_root_relaxation_solution_callback" cpp/src/mip_heuristics/ -B5 -A10Repository: NVIDIA/cuopt
Length of output: 9581
🏁 Script executed:
# Check if MIP row scaling is documented as internal-only or if it affects user-visible duals
rg "row_scaling" cpp/include/cuopt/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 38
🏁 Script executed:
# Look at how the scaled problem is used - is the scaling meant to be transparent or not?
rg "is_scaled_" cpp/src/mip_heuristics/problem/ -B2 -A2Repository: NVIDIA/cuopt
Length of output: 1740
Store and apply accumulated MIP row scaling factors for post-solve dual unscaling.
Each iteration applies iteration_scaling in-place to coefficients and bounds, but the composite per-row scaling product is never stored in op_problem_scaled_ or accessible later. If LP dual variables from PDLP sub-solves are used by branch-and-bound or callbacks for sensitivity analysis or branching decisions (e.g., via reduced costs), they will correspond to the scaled problem and require unscaling via the stored row scaling factors to match the original problem space.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu` around lines 327 - 370, You
apply per-iteration scaling (iteration_scaling) to coefficients and bounds but
never record the accumulated per-row scaling factors, so later dual unscaling
can't be performed; add and update a per-row accumulator in op_problem_scaled_
(e.g., op_problem_scaled_.row_scaling_factors initialized to 1) and multiply it
by the iteration scaling for each row each iteration. Concretely: locate
iteration_scaling, coefficient_row_index, and the three thrust::transform calls
that multiply op_problem_scaled_.coefficients, .constraint_lower_bounds, and
.constraint_upper_bounds; add a thrust::transform to update
op_problem_scaled_.row_scaling_factors by applying iteration_scaling (use a
permutation iterator with coefficient_row_index when mapping scalings through
coefficient indices to row indices for coefficients, and a direct iterator for
constraint bounds) so the per-row product is maintained for post-solve dual
unscaling.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu`:
- Around line 146-150: Thrust kernel launches in mip_scaling_strategy.cu (e.g.,
the thrust::transform that uses handle_ptr->get_thrust_policy() and
min_nonzero_bounds_abs_transform_t<f_t>{}) are missing explicit CUDA error
checks; after each Thrust call (all thrust::transform / thrust::copy /
thrust::fill invocations at the reported locations) add a CUDA
device-synchronization error check using the same pattern as CUB calls (e.g.,
wrap a cudaDeviceSynchronize() or thrust::detail::backend::cuda::sync call
followed by RAFT_CUDA_TRY or
CUDA_CHECK(cudaGetLastError())/CUDA_CHECK(cudaDeviceSynchronize()) ) so any
launch or runtime error is caught and propagated—apply this change to each
affected symbol/site in the file (the transform using
min_nonzero_bounds_abs_transform_t, other thrust::transform/copy/fill
invocations around lines reported) to ensure consistent error handling.
- Around line 300-309: Clamp the user-provided
row_scaling_k_min/row_scaling_k_max to a safe exponent range before calling
ldexp and before passing into the kernel: compute safe_k_min/safe_k_max by
clamping row_scaling_k_min/row_scaling_k_max to a predetermined safe exponent
window (e.g., the sensible defaults like -20..20 or the numeric limits for f_t's
exponent range), then use safe_k_min/safe_k_max when computing
min_scaling/max_scaling (instead of raw row_scaling_k_min/row_scaling_k_max),
include those safe values in the kernel lambda capture, and replace the per-row
k and required_k clamps with clamps against safe_k_min/safe_k_max so ldexp(1,k)
cannot underflow/overflow.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
cpp/src/mip_heuristics/diversity/diversity_manager.cucpp/src/mip_heuristics/diversity/diversity_manager.cuhcpp/src/mip_heuristics/mip_scaling_strategy.cucpp/src/mip_heuristics/mip_scaling_strategy.cuh
| thrust::transform(handle_ptr->get_thrust_policy(), | ||
| row_bounds_begin, | ||
| row_bounds_end, | ||
| row_min_nonzero_bounds.begin(), | ||
| min_nonzero_bounds_abs_transform_t<f_t>{}); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the file structure and the specific lines mentioned
wc -l cpp/src/mip_heuristics/mip_scaling_strategy.cuRepository: NVIDIA/cuopt
Length of output: 106
🏁 Script executed:
# Read the section around lines 146-150 to see the context
sed -n '140,160p' cpp/src/mip_heuristics/mip_scaling_strategy.cuRepository: NVIDIA/cuopt
Length of output: 1229
🏁 Script executed:
# Search for all thrust:: launches in the file and check if they are followed by RAFT_CUDA_TRY
cat cpp/src/mip_heuristics/mip_scaling_strategy.cu | grep -n "thrust::" | head -20Repository: NVIDIA/cuopt
Length of output: 1476
🏁 Script executed:
# More detailed check: look for the pattern of thrust launches and nearby error checks
python3 <<'EOF'
import re
with open('cpp/src/mip_heuristics/mip_scaling_strategy.cu', 'r') as f:
lines = f.readlines()
# Find all thrust:: calls with context
results = []
for i, line in enumerate(lines):
if 'thrust::' in line and not line.strip().startswith('//'):
# Look at the next few lines to see if there's a RAFT_CUDA_TRY
context_start = max(0, i - 1)
context_end = min(len(lines), i + 5)
context = ''.join(lines[context_start:context_end])
# Check if RAFT_CUDA_TRY appears in the next few lines
has_check = 'RAFT_CUDA_TRY' in context
results.append({
'line': i + 1,
'code': line.strip(),
'has_check': has_check,
'context': context.strip()
})
# Print summary
print("Thrust launches found:")
print("=" * 80)
for r in results:
print(f"Line {r['line']}: {r['code'][:70]}")
print(f" Has error check: {r['has_check']}")
print()
EOFRepository: NVIDIA/cuopt
Length of output: 4849
🏁 Script executed:
# Also check what RAFT_CUDA_TRY pattern looks like in this file
grep -n "RAFT_CUDA_TRY" cpp/src/mip_heuristics/mip_scaling_strategy.cu | head -10Repository: NVIDIA/cuopt
Length of output: 626
🏁 Script executed:
# Check the specific ranges mentioned in the comment
echo "=== Line 146-150 ==="
sed -n '146,150p' cpp/src/mip_heuristics/mip_scaling_strategy.cu
echo ""
echo "=== Line 207-225 ==="
sed -n '207,225p' cpp/src/mip_heuristics/mip_scaling_strategy.cu
echo ""
echo "=== Line 320-321 ==="
sed -n '320,321p' cpp/src/mip_heuristics/mip_scaling_strategy.cu
echo ""
echo "=== Line 325-336 ==="
sed -n '325,336p' cpp/src/mip_heuristics/mip_scaling_strategy.cuRepository: NVIDIA/cuopt
Length of output: 1835
🏁 Script executed:
# Check whether this file uses a specific pattern - perhaps thrust operations are wrapped at a higher level
grep -B2 -A2 "thrust::transform\|thrust::upper_bound\|thrust::count\|thrust::fill" cpp/src/mip_heuristics/mip_scaling_strategy.cu | head -50Repository: NVIDIA/cuopt
Length of output: 2085
🏁 Script executed:
# Check if there's any wrapper or special handling in thrust_policy
find . -type f -name "*.hpp" -o -name "*.h" | xargs grep -l "thrust_policy\|get_thrust_policy" | head -5Repository: NVIDIA/cuopt
Length of output: 222
🏁 Script executed:
# Check the RAFT handle implementation to see if get_thrust_policy includes error checking
rg "get_thrust_policy|thrust_policy" -A 3 -B 1 --max-count=10Repository: NVIDIA/cuopt
Length of output: 50369
🏁 Script executed:
# Look for any error checking patterns after thrust operations in the broader codebase
rg "thrust::(transform|fill|upper_bound|count|transform_reduce)" -A 2 | grep -E "CUDA|cuda|error|check" | head -20Repository: NVIDIA/cuopt
Length of output: 1439
🏁 Script executed:
# Specifically check other scaling/heuristics files in the same directory for comparison
ls -la cpp/src/mip_heuristics/Repository: NVIDIA/cuopt
Length of output: 1531
🏁 Script executed:
# Check how other files in the same directory handle Thrust launches
find cpp/src/mip_heuristics -name "*.cu" -o -name "*.cuh" | xargs grep -l "thrust::" | head -3Repository: NVIDIA/cuopt
Length of output: 210
🏁 Script executed:
# Sample one other file to see if this is a systemic pattern
head -250 cpp/src/mip_heuristics/mip_scaling_strategy.cu | tail -50 | rg "thrust::" -A 1Repository: NVIDIA/cuopt
Length of output: 857
🏁 Script executed:
# More comprehensive check: do ANY thrust calls in this repo have post-call error checks?
rg "thrust::(transform|fill|upper_bound|count)" -A 1 | grep -B1 "RAFT_CUDA_TRY\|cudaPeekAtLastError" | head -20Repository: NVIDIA/cuopt
Length of output: 38
Add explicit CUDA error checks after Thrust launches.
CUB calls are checked with RAFT_CUDA_TRY, but Thrust kernel launches lack explicit error verification. This inconsistency can hide launch and runtime failures, weakening error propagation.
🔧 Minimal fix pattern
thrust::transform(handle_ptr->get_thrust_policy(),
row_bounds_begin,
row_bounds_end,
row_min_nonzero_bounds.begin(),
min_nonzero_bounds_abs_transform_t<f_t>{});
+ RAFT_CUDA_TRY(cudaPeekAtLastError());Apply the same pattern after each Thrust launch.
Affected locations: 146–150, 207–225, 320–321, 325–336, 331–336, 352–353, 374–387, 396–402, 462–468, 470–475, 476–481.
Per coding guidelines: "Every CUDA kernel launch and memory operation must have error checking with CUDA_CHECK or equivalent verification" and "Verify error propagation from CUDA to user-facing APIs is complete."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| thrust::transform(handle_ptr->get_thrust_policy(), | |
| row_bounds_begin, | |
| row_bounds_end, | |
| row_min_nonzero_bounds.begin(), | |
| min_nonzero_bounds_abs_transform_t<f_t>{}); | |
| thrust::transform(handle_ptr->get_thrust_policy(), | |
| row_bounds_begin, | |
| row_bounds_end, | |
| row_min_nonzero_bounds.begin(), | |
| min_nonzero_bounds_abs_transform_t<f_t>{}); | |
| RAFT_CUDA_TRY(cudaPeekAtLastError()); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cpp/src/mip_heuristics/mip_scaling_strategy.cu` around lines 146 - 150,
Thrust kernel launches in mip_scaling_strategy.cu (e.g., the thrust::transform
that uses handle_ptr->get_thrust_policy() and
min_nonzero_bounds_abs_transform_t<f_t>{}) are missing explicit CUDA error
checks; after each Thrust call (all thrust::transform / thrust::copy /
thrust::fill invocations at the reported locations) add a CUDA
device-synchronization error check using the same pattern as CUB calls (e.g.,
wrap a cudaDeviceSynchronize() or thrust::detail::backend::cuda::sync call
followed by RAFT_CUDA_TRY or
CUDA_CHECK(cudaGetLastError())/CUDA_CHECK(cudaDeviceSynchronize()) ) so any
launch or runtime error is caught and propagated—apply this change to each
affected symbol/site in the file (the transform using
min_nonzero_bounds_abs_transform_t, other thrust::transform/copy/fill
invocations around lines reported) to ensure consistent error handling.
373d374 to
cfcf0ab
Compare
This PR adds row scaling to MIP and uses PDLP scaling for both barrier and PDLP.
Summary by CodeRabbit
New Features
Bug Fixes
Chores